\chapter{API Design Principles}

% \tocpart{API Design Principles}

API 设计规范

\begin{quote}
译者注：

本文不来自于 Qt 文档，而是来自于 Qt Wiki：\href{https://wiki.qt.io/API_Design_Principles}{API\_Design\_Principles}

API(Application Programming Interface)，应用开发接口，本文中也将 P 解释为 Programmer（开发者）。	
\end{quote}

Qt 最出名的特点之一是一致性强、易于学习、功能强大的 API。本文尝试对我们在设计 Qt 风格的 API 中积累的诀窍进行总结。其中许多准则都是通用的，其它的则是习惯性用法，我们主要为了保持接口一致性而继续遵循。

尽管这些准则主要面向公共接口，但也鼓励您在设计内部接口时使用相同的技术，这对与您合作开发的同僚会更加友好。

您可能也会有兴趣查阅 Jasmin Blanchette 的 
\href{https://people.mpi-inf.mpg.de/~jblanche/api-design.pdf}{Little Manual of API Design (PDF)}，或它的前身，由 Matthias Ettrich 编写的 \href{https://doc.qt.io/archives/qq/qq13-apis.html}{Designing Qt-Style C++ APIs} 。

\section{优秀接口的六大特点}

API 是面向开发者的，而 GUI 则是面向终端用户。API 中的 P 代表开发者(Programmer)，而非程序(Program)，目的是指出 API 由开发者，即人类（而非计算机）所使用这一特点。

Matthias 在 Qt 季刊弟13期，关于 API 设计的文章中，声称他坚信 API 应该是最小化但完备的，具备清晰而简洁的语义，符合直觉，被开发者，易于记忆，能够引导开发者编写高可读性的代码。

\subsection{最小化}

最小化的 API 指包含尽可能少的公共成员和最少的类。这可让 API 更易于理解、记忆、调试和修改。

\subsection{完备性}
完备的 API 意味着具备所有应有的功能。这可能会与最小化产生冲突。此外，若一个成员函数位于错误的类中，则大多数潜在的用户回无法找到它。

\subsection{清晰简洁的语义}

与其它设计协作时，应该让您的设计做到最小例外。通用化会让事情更简单。特例可能存在，但不应成为关注的焦点。在处理特定问题时，不应让解决方案过度泛化。（例如，Qt 3 中的 QMineSourceFactory 本该被称作 QImageLoader，并且具备另一套 API。）

\subsection{复合直觉}

如同计算机上其它内容，API 应复合直觉。不同的开发经验和技术背景会导致对复合直觉与否有不同的感知。复合直觉的 API 应能让中等经验的开发者无需阅读文档并直接使用，并让不知道这个 API 的开发者可以理解使用它编写的代码。

\subsection{易于记忆}

为了让 API 易于记忆，请选择一组保持一致并足够精确的命名约定。使用可理解的模式和概念，并且避免缩写。

\subsection{可读性导向}

编写代码只需要一次，但阅读（以及调试和修改）则会非常频繁。高可读性的代码通常需要花更多时间编写，但可以在产品的生命周期中节约更多的时间。

最后需要谨记，不同类型的用户会使用 API 的不同部分。在单纯地创建一个 Qt 类实例能非常直观的同时，希望用户在尝试继承它之前先阅读文档则是很合理的。

\subsection{静态多态}

相似的代码类应具有相似的 API。可以使用继承来实现——当运行时多态支持时，这是很合理的。但多态同时也可以体现在设计截断。例如，若将代码中的 QProgressBar 换为 QSlider，或将 QString 换为 QByteArray，他们间相似的 API 会另替换操作变得非常容易。这便是为何我们称之为“静态多态”。

静态多态同样可让记忆 API 和开发模式变得更加简单。结果是，对于一组有关联的类，相似的 API 通常比为每个类独立设计的完美 API 更加好用。

在 Qt 中，当不具备有足够说服力的原因时，我们更倾向于使用静态多态，而非继承。这减少了 Qt 的公共类数量，并让新用户更容易在文档中找到需要的内容。

好的

​ QDialogBox 和 QMessageBox 具有相似的 APi，以用于处理按钮（addButton(), setStandardButtons()，但无需继承自某些 "QAbstractButtonBox" 类。

坏的

​ QAbstractSocket 被 QTcpSOcket 和 QUdpSocket 所继承，但这两个类的交互方式差异很大。看起来并没有人使用过（或能够使用） QAbstractSocket 指针来进行通用且有效的操作。

存疑

​ QBoxLayout 是 QHBoxLayout 和 QVBoxLayout 的基类。优点：可以在工具栏中使用 QBoxLayout，调用 setOrientation() 来令其水平/垂直排布。缺点：引入额外的类，用户可能会写出形如 ((QBoxLayout *)hbox)->setOrientation(Qt::Vertical) 的代码，而这是不合理的。

\section{基于属性的 API}

比较新的 Qt 类倾向于使用基于属性的 API，例如：

\begin{cppcode}
QTimer timer;
timer.setInterval(1000);
timer.setSingleShot(true);
timer.start();
\end{cppcode}

此处的属性，指的是作为对象状态一部分的任何概念性的特征——无论是否是实际的 Q\_PROPERTY。只要可行，用户都应该允许以任何顺序设置属性，也就是说，这些属性应该是正交的。例如，上文的代码也可以写为：

\begin{cppcode}
QTimer timer;
timer.setSingleShot(true);
timer.setInterval(1000);
timer.start();
\end{cppcode}

为了方便，我们也可以这样写：

\begin{cppcode}
timer.start(1000);
\end{cppcode}

类似的，对于 QRegExp，我们可以：

\begin{cppcode}
QRegExp regExp;
regExp.setCaseSensitive(Qt::CaseInsensitive);
regExp.setPattern(".");
regExp.setPatternSyntax(Qt::WildcardSyntax);
\end{cppcode}

为了实现此类 API，内部的对象需要被惰性构造。例如在 QRegExp 的案例中，不应该在还不知道表达式使用何种语法之前，就在 setPattern() 中过早地编译 . 表达式。

属性通常是级联的，此时我们应该谨慎地处理。仔细考虑下当前样式提供的“默认图标大小”与 QToolButton 的 iconSize 属性：

\begin{cppcode}
toolButton->iconSize(); // 返回当前样式表的默认大小
toolButton->setStyle(otherStyle);
toolButton->iconSize(); // 返回 otherStyle 的默认大小
toolButton->setIconSize(QSize(52, 52));
toolButton->iconSize(); // 返回 (52, 52)
toolButton->setStyle(yetAnotherStyle);
toolButton->iconSize(); // 返回 (52, 52) 
\end{cppcode}


\begin{notice}
	一旦 iconSize 被设置，它会被一直留存，此时修改当前样式不会影响它。这是 好的。有时，提供重置属性的渠道会很方便，这有两种实现方式：

\end{notice}

\begin{compactitem}
	\item 传递一个特定值（如 QSIze()、-1 或 Qt::Alignment(0)）来指代“重置”；
	\item 提供显示的 resetFoo() 或 unsetFoo() 函数。
\end{compactitem}

对于 iconSize，将 QSize()（即 QSize(-1, -1)）设为“重置”便足够了。

某些场景中，取值方法会返回值会与设置的内容不同。例如，若调用 widget->setEnabled(true)，可能通过 widget->isEnabled() 获得的依然是 false，因为父控件被禁用了。这并没问题，因为通常这正是我们要检查的状态（父控件被禁用时，子控件也应该变灰，表现为也被禁用，但同时在它内部，应该知道自己实际是“可用”的，并等待父控件可用后恢复状态），但必须在文档中正确地进行描述。

QProperty

\begin{quote}
译者注：该类型为 Qt 6.0 引入，需要参见 Qt 6 类文档。

本文原文中的内容与现有的 Qt 6.0 预览版存在出入，因此暂不翻译本节，待官方进一步维护更新原文。
\end{quote}

\section{C++ 特性}

\subsection{值 与 对象}

\begin{quote}
译者注：此条原文无内容，待官方更新
\end{quote}

\subsection{指针 与 引用}

作为输出参数，指针与引用哪个更好？

\begin{cppcode}
void getHsv(int *h, int *s, int *v) const void getHsv(int &h, int &s, int &v) const
\end{cppcode}

绝大多数 C++ 数据都推荐尽可能使用引用，因为引用在感知上比指针“更安全更漂亮”。事实上，我们在 Qt 软件中更倾向于使用指针，因为这会令代码更加已读。如对比：


\begin{cppcode}
color.getHsv(&h, &s, &v);
color.getHsv(h, s, v);
\end{cppcode}

第一行代码能很清晰地表示，h、s、v 对象有很大概率会被该函数调用所修改。

即便如此，由于编译器并不喜欢输出参数，在新 APi 中应该避免此用法，而是返回一个小结构体：

\begin{cppcode}
struct Hsv { int hue, saturation, value }; Hsv getHsv() const;
\end{cppcode}

\begin{quote}
	译者注：对于可能失败的带返回值的函数，Qt 倾向于返回数值，使用 bool* ok = 0 参数来存储调用结果，以便在不关心时忽略之。同样在 Qt 6 以后，该方式不再被建议使用，而是改用 std::optional<T> 返回类型。
\end{quote}

\subsection{传递常引用 与 传递值}
若类型大于16字节，传递常引用。

若类型具有非平凡的拷贝构造函数或非平凡的析构函数，传递常引用来避免执行这些方法。

所有其它类型都应使用值传递。

范例：

\begin{cppcode}
void setAge(int age);
void setCategory(QChar cat);
void setName(QLatin1String name);
void setAlarm(const QSharedPointer<Alarm> &alarm); // 常引用远快于拷贝构造和析构
// QDate, QTime, QPoint, QPointF, QSize, QSizeF, QRect 都是其它应该值传递的好例子
\end{cppcode}

\section{虚函数}

当 C++ 中的一个成员函数被声明为虚函数，这主要用于通过在子类中重写来自定义该函数的行为。将函数设为虚函数的目的是让对该函数的现有调用会被替代为访问自定义的代码分支。若在该类之外没人调用此函数，则在将其声明为虚函数之前需要小心斟酌：

\begin{cppcode}
// Qt 3 中的 QTextEdit: 成员函数不需要作为虚函数的成员函数
virtual void resetFormat();
virtual void setUndoDepth( int d );
virtual void setFormat( QTextFormat &f, int flags );
virtual void ensureCursorVisible();
virtual void placeCursor( const QPoint &pos;, QTextCursorc = 0 );
virtual void moveCursor( CursorAction action, bool select );
virtual void doKeyboardAction( KeyboardAction action );
virtual void removeSelectedText( int selNum = 0 );
virtual void removeSelection( int selNum = 0 );
virtual void setCurrentFont( const QFont &f );
virtual void setOverwriteMode( bool b ) { overWrite = b; }
\end{cppcode}

当 QTextEdit 从 Qt 3 迁移至 Qt 4 时，几乎所有虚函数都被移除。有趣的是（但并未预料到），并没有大量的抱怨。为什么？因为 Qt 3 并未使用 QTextEdit 的多态性，Qt 3 并不会调用这些函数——只有使用者会。简单来说，否则并没有任何理由去继承 QTextEdit 并重写这些方法——除非您自己会通过多态去调用它们。若您需要在您的应用程序，也就是 Qt 之外使用多态机制，您应该自行添加。

避免使用虚函数

在 Qt 中，我们因为多种原因而尝试最小化虚函数的数量。每个虚函数调用都会让缺陷修复变得更难，因为会在调用图中插入一个不受控制的节点（使得调用结果无法预测）。人们会在虚函数中做非常疯狂的举措，例如：

\begin{compactitem}
\item 发送事件
\item 发送信号
\item 重入事件循环（例如，打开一个模态文件对话框）
\item 删除对象（例如，某些导致 delete this 的操作）
\end{compactitem}

此外还有一些避免过度使用虚函数的原因：

\begin{compactitem}
\item 无法在不破坏二进制兼容性的前提下增加、移动或删除虚函数
\item 无法简便地重写虚函数
\item 编译器通常几乎不会内敛虚函数调用
\item 调用虚函数需要查询虚表，导致其比常规函数调用慢2-3倍
\item 虚函数另类对象更难进行值拷贝（可能做到，但会表现得很混乱且不被推荐）
\end{compactitem}

过去的经验告诉我们，没有虚函数地类会产生更少的错误，通常也导致更少的维护。

一个通用的经验法则是，除非从工具集或该类的主要使用者角度需要调用它，否则一个函数不应该是虚函数。

\section{虚对象 与 可复制性}

多态对象和值类型的类并不是好朋友。

包含虚函数的类会有虚析构函数来避免基类析构时未清理子类数据导致的内存泄漏。

若需要以值语义拷贝、复制和对比一个类，则可能需要拷贝构造函数、赋值运算符重载和等于运算符重载：

\begin{cppcode}
class CopyClass {
	public:
	CopyClass();
	CopyClass(const CopyClass &other);
	~CopyClass();
	CopyClass &operator=(const CopyClass &other);
	bool operator== (const CopyClass &other) const;
	bool operator!=(const CopyClass &other) const;
	virtual void setValue(int v);
};
\end{cppcode}


若创建该类的子类，则代码中会开始发生意料外的行为。通常来说，若没有虚函数和虚构造函数，则人们无法创建依赖于多态特性的子类。因此一旦虚函数或虚析构函数被添加，这会马上成为建立子类的理由，事情从此变得复杂。乍一看来，很容易觉得可以简单定义一下虚运算符重载。但顺着这条路深入下去，会导致混乱和毁灭（例如无可读性的代码）。请研究下这段代码：

\begin{cppcode}
class OtherClass {
	public:
	    const CopyClass &instance() const; // 此处会返回什么？我应该将返回值赋值给谁？
};
\end{cppcode}
（此小节正在施工中）

\section{不变性}

C++ 提供了 const 关键字来标识不会改变或不会产生副作用的事物。它可被用于数值、指针和倍只想的内容，也可被作为成员函数的特殊属性来标识它不会修改对象的状态。

\begin{notice}
const 自身并不提供太大的价值——许多语言甚至并未提供 const 关键字，但这并不会自动导致它们存在缺陷。事实上，若移除所有函数重载，并通过搜索替换移除 C++ 代码中的所有 const 标识，代码很可能依然能够编译并正确执行。使用实用主义导向来使用 const 是很重要的。
\end{notice}

让我们看看 Qt 中使用 const 的 API 设计：

输入参数：const 指针

使用指针输入参数的 const 成员函数几乎总是使用 const 指针。

若一个成员函数确实被声明为 const，这意味着它不具有副作用，也不会修改对象对外可见的状态。那么，为什么要需要非 const 的输入参数？需要牢记，const 成员函数经常会被其它 const 成员函数，在这些调用场合中，非 const 的指针并不容易得到（除非使用 const\_cast，而我们应该尽可能避免使用它）。

修改之前：

\begin{cppcode}
bool QWidget::isVisibleTo(const QWidget *ancestor) const;
bool QWidget::isEnabledTo(const QWidget *ancestor) const;
QPoint QWidget::mapFrom(const QWidget *ancestor, const QPoint &pos) const;
\end{cppcode}

\begin{notice}
我们在 QGraphicsItem 中修复了这些成员函数，但 QWidget 的修复需要等待 Qt 5：
\end{notice}


\begin{cppcode}
bool isVisibleTo(const QGraphicsItem *parent) const;
QPointF mapFromItem (const QGraphicsItem *item, const QPointF &point) const; 
\end{cppcode}

返回值：const 值

函数的返回值，要么是引用类型，要么是右值。

非类类型的右值是不受cv限定符影响的。因此，即使在语法上允许为其添加 const 修饰，这并不会产生效果，因为由于其访问权不允许对其做出修改。大多数现代编译器在编译此类代码时会打印警告信息。

当为类类型的右值添加 const 时，对该类的非 const 的成员函数的访问会被禁止，对其成员变量的直接操作也会被禁止。

不添加 const 允许此类操作，但很少有此类需求，因为这些修改会伴随右值对象生命周期的结束而消失，这会在当前语句的分号结束后发生。

例如：

\begin{cppcode}
struct Foo {
	void setValue(int v) { value = v; } int value;
};

Foo foo() { return Foo(); }
const Foo cfoo() { return Foo(); }
int main() {
	// 下述代码可以编译，foo() 返回非 const 右值，无法
	// 成为赋值目标（这通常需要左值），但对成员变量的访问
	// 是左值：
	foo().value = 1; // 可以编译，但该临时值在这个完整的表达式结束后会被抛弃
	
	// 下述代码可以编译，foo() 返回非 const 右值，无法
	// 成为赋值目标，但可以调用（甚至于非 const 的)成员函数：
	foo().setValue(1); // 可以编译，但该临时值在这个完整的表达式结束后会被抛弃
	
	// 下述代码无法编译，cfoo() 返回 const 右值，因此其
	// 成员变量是 const 授权，无法被赋值：
	cfoo().value = 1; // 无法编译
	
	// 下述代码无法编译，cfoo() 返回 const 右值，无法调用
	// 其非 const 的成员函数：
	cfoo().setValue(1); // 无法编译
}
\end{cppcode}

返回值：指针与 const 指针

const 成员函数应该返回指针还是 const 指针这个问题，令许多人发现 C++ 的“const 正确性”被瓦解了。该问题源于某些 const 成员函数，并不修改它们的内部状态，而是返回成员变量的非 const 指针。单纯返回一个指针并不会影响对象对外可见的状态，也不会修改它正在维护的状态。但这会令程序员获得间接地修改对象数据的权限。

下述范例展示了通过 const 成员函数返回的非 const 指针来规避不可变性的诸多方法之一：

\begin{cppcode}
QVariant CustomWidget::inputMethodQuery(Qt::InputMethodQuery query) const {
	moveBy(10, 10); // 无法编译！
	window()->childAt(mapTo(window(), rect().center()))->moveBy(10, 10); // 可以编译！
}
\end{cppcode}

返回 const 指针的函数，至少在一定程度上，避免了此类（可能并不希望/非预期的）副作用。但哪些函数会考虑返回 const 指针，或一组 const 指针？若我们使用 const正确 的方案，即令任何 const 成员函数返回成员变量（或一组成员变量的指针）时都是用 const 指针形式。很不幸的是，实际中这会造就无法使用的 API：

\begin{cppcode}
QGraphicsScene scene;
// … 初始化场景
foreach (const QGraphicsItem *item, scene.items()) {
	item->setPos(qrand() % 500, qrand() % 500); // 无法编译！item 是 const 指针
}
\end{cppcode}

QGraphicsScene::items() 是 const 成员函数，这可能会让人觉得应该返回 const 指针。

在 Qt 中，我们近乎只使用非 const 的模式。我们选择了实用主义之路：返回 const 指针更容易导致 const\_cast 的过度使用，这比滥用返回的非 const 指针引发的问题更加频繁。

返回类型：值 或 const 引用？

如果我们在返回对象时还保留了它的副本，返回 const 引用是最快的方法；然而，这在我们之后打算重构这个类时成为了限制（使用 d 指针惯用法，我们可以在任何时候修改 Qt 类的内存结构；但我们无法在不破坏二进制兼容性的前提下，将函数签名从 const QFoo\& 改为 QFoo）。出于此原因，我们通常返回 QFoo 而非 const QFoo\&，除了性能极端敏感，而重构并不是问题的少数场合（例如 QList::at()）。

const 与 对象的状态

const正确性 是 C 中的一场“圣战”（译者注：原文为 vi-emacs discussion），因为该原则在一些领域（如基于指针的函数）中是失效了。

但 const 成员函数的常规含义是值不会修改一个类对外可见的状态，状态在此处值“我自己的和我负责的”。这并不意味着 const 成员函数会改变它们自己的私有成员变量，但也不代表不能这么做。但通常来说，const 成员函数不会产生可见的副作用。例如：

\begin{cppcode}
QSize size = widget->sizeHint(); // const
widget->move(10, 10); // 非 const
\end{cppcode}

代理对象负责处理对另一个对象的绘制工作，它的状态包含了它负责的内容，也就是包含它的绘制目标的状态。请求代理进行绘制是具有副作用的：这会改变正在被绘制的设备的外观（也意味着状态）。正因如此，令 paint() 成为 const 并不合理。任何视图控件或 QIcon 的 paint() 作为 const 都很不合理。没人会在 const 成员函数中去调用 QIcon::paint()，除非他们明确地像规避当前函数的 const 性质。而在这种场景中，显示的 const\_cast 会是更好的选择：

\begin{cppcode}
// QAbstractItemDelegate::paint 是 const
void QAbstractItemDelegate::paint(QPainter *painter,
const QStyleOptionViewItem &option,
const QModelIndex &index) const
 
// QGraphicsItem::paint 不是 const
void QGraphicsItem::paint(QPainter &painter, 
const QStyleOptionGraphicsItem &option, 
QWidget &widget = 0)
\end{cppcode}

const 关键字不会为你做任何事，考虑将其移除，而非为一个成员函数提供 const/非 const 的重载版本。

